<?php
namespace WDB\Wrapper;
use WDB,
    WDB\Exception;

/**
 * @author Richard Ejem <richard(at)ejem.cz>
 * @package WDB
 */
class Table extends WDB\BaseObject implements iDBTable
{
    /**@var WDB\Database $database */
    protected $database;

    /**@var WDB\Validation\iValidator[] $customValidators*/
    protected $customValidators = array();

    public function getDatabase()
    {
        return $this->database;
    }

    /**@var Analyzer\Schema */
    protected $schema;
    /**@var Analyzer\TableView */
    protected $tableAnalyzer;
    /**@var string[] array of names of columns which represents primary access key to this table*/
    protected $key;
    /**@var \WDB\Structure\TableWrapperCache
     @deprecated*/
    protected $cache;

    /**@var bool whether or not to use cache for bound db data (table loads only once per object)
     * @deprecated
     */
    protected $useCache = TRUE; //TODO implement way to change

    /**@var WDB\Query\Element\SelectField[] fields/expressions that will be loaded from database
     * when browsing this table by ::getDatasource() method */
    protected $readFields;

    /**@var string[] column wrapper class names container. Can be used in children to specify
     *  wrapper class to a column.
     */
    protected $columnWrapperClasses = array();

    /**@var string[] column webui class names container. Can be used in children to specify
     *  webui class to a column.
     */
    protected $columnWebUIClasses = array();

    /**@var Column[]*/
    protected $columns = array();

    /**@var string[] column alias => canonical name */
    protected $columnAliases = array();

    protected function configureDatasource(WDB\iDatasource $datasource, array $cfg)
    {
        foreach ($cfg as $key=>$val)
        {
            if ($val === NULL) continue;
            switch ($key)
            {
                case 'page':
                    $datasource->page($val);
                    break;
                case 'sort':
                    if (!isset($this->columns[$val]) || !$this->columns[$val] instanceof iOrderable || !$this->columns[$val]->isOrderable())
                            throw new Exception\BadArgument('table '.$this->getName().' cannot be sorted by '.$val);
                    $this->columns[$val]->sortDatasource($datasource, $cfg['asc']);
                    break;
                case 'filter':
                    foreach ($val as $filterKey=>$filterVal)
                    {
                        if (!isset($this->columns[$filterKey]) || !$this->columns[$filterKey] instanceof iFilterable || !$this->columns[$filterKey]->isFilterable())
                                throw new Exception\BadArgument('table '.$this->getName().' cannot be filtered by '.$filterKey);
                        $this->columns[$filterKey]->filterDatasource($datasource, $filterVal);
                    }
                    break;
            }
        }
    }

    // <editor-fold defaultstate="collapsed" desc="iDBTable implementation">

    public function getName()
    {
        return $this->tableAnalyzer->name;
    }

    public function getTitle()
    {
        return $this->tableAnalyzer->title;
    }

    public function serializeKey(array $k)
    {
        return WDB\Utils\Strings::serializeCommaSeparatedList($k);
    }

    public function unserializeKey($data, array $keys)
    {
        return WDB\Utils\Strings::unserializeCommaSeparatedList($data, $keys);
    }

    /**
     *
     * @throws Exception\ColumnNotFound
     * @throws Exception\NotFound
     */
    public function getRecordByKey($key)
    {
        $key = $this->canonicalizeKey($key);
        $result = $this->getDatasource()->filter($key)->run($this->database);
        if (count($result) == 0) throw new Exception\NotFound ("record not found.");
        return $this->fetchRecord($result->singleRow());
    }

    protected function canonicalizeKey($key)
    {
        if(!is_array($key))
        {
            $pk = $this->tableAnalyzer->getPrimaryKey();
            if (count($pk) > 1) throw new Exception\BadArgument("Cannot use single-value primary key on a table with multi-value primary key");
            return array($pk[0]=>$key);
        }
        return $key;
    }

    public function getRecordByStringPK($key)
    {
        $key = $this->unserializeKey($key, $this->getPrimaryKey());
        return $this->getRecordByKey($key);
    }

    public function getPrimaryKey()
    {
        return $this->tableAnalyzer->getPrimaryKey();
    }

    public function getUniqueKeys()
    {
        return $this->tableAnalyzer->getUniqueKeys();
    }

    public function fetchRecord(WDB\Query\SelectedRow $row)
    {
        return RecordFactory::load($this, $row);
    }

    public function newRecord($data = array(), $mode = WDB\Query\Insert::UNIQUE)
    {
        return RecordFactory::create($this, $data, $mode);
    }

    public function elevateRecords($rows)
    {
        return new RecordCollection($rows, $this);
    }

    public function getColumnWrapperClass($columnName)
    {
        if (isset($this->columnWrapperClasses[$columnName])) return $this->columnWrapperClasses[$columnName];
        if (isset($this->tableAnalyzer->columns[$columnName]))
        {
            return $this->tableAnalyzer->columns[$columnName]->getWrapperClass();
        }
        return NULL;
    }

    public function getColumnWebUIClass($columnName)
    {
        if (isset($this->columnWebUIClasses[$columnName])) return $this->columnWebUIClasses[$columnName];
        $columns = $this->tableAnalyzer->getColumns();
        if (isset($columns[$columnName]))
        {
            return $columns[$columnName]->getWebUIClass();
        }
        return NULL;
    }

    public function getWebUIClass()
    {
        return $this->tableAnalyzer->getWebUIClass();
    }

    public function getRecordWrapperClass()
    {
        return $this->tableAnalyzer->getRecordWrapperClass();
    }

    public function getColumns()
    {
        return $this->columns;
    }

    public function getColumnCName($alias)
    {
        if (isset($this->columnAliases[$alias])) return $this->columnAliases[$alias];
        return $alias;
    }

    public function isListedInSchemaAdmin()
    {
        return $this->tableAnalyzer->isListedInSchemaAdmin();
    }

    public function isWebUIAccessible()
    {
        return $this->tableAnalyzer->isWebUIAccessible();
    }

    public function fetchValidators(WDB\Event\Event $event)
    {
        $this->tableAnalyzer->fetchValidators($event);
        foreach ($this->customValidators as $validator)
        {
            $event->addListener($validator);
        }
    }

    public function addValidator(WDB\Validation\iValidator $validator)
    {
        $this->customValidators[] = $validator;
    }

    public function getTableAnalyzer()
    {
        return $this->tableAnalyzer;
    }

    public function addColumn (iColumn $column, $name = NULL)
    {
        if ($name === NULL)
        {
            $name = $column->name;
        }
        $this->columns[$name] = $column;

        if ($column instanceof iDBColumn)
        {
            $column->fetchReadFields($this->readFields);
        }
    }

    // </editor-fold>

    /**
     * initializes readFields protected property. Override this to make own fields list in your table wrapper
     */
    protected function initReadFields()
    {
        $this->readFields = array();
        foreach ($this->columns as $column)
        {
            if ($column instanceof iDBColumn)
            {
                $column->fetchReadFields($this->readFields);
            }
        }
    }

    public function __construct(WDB\Analyzer\iTable $tableAnalyzer)
    {
        $this->schema = $tableAnalyzer->getSchema();
        $this->database = $this->schema->getDatabase();

        $this->tableAnalyzer = $tableAnalyzer;
        $this->key = $this->tableAnalyzer->getPrimaryKey();
        $this->cache = new WDB\Structure\TableWrapperCache();

        $this->initBasicColumns($tableAnalyzer->getColumns());
        $this->initForeignColumns($tableAnalyzer->getForeignKeys());


        $this->initReadFields();

        $this->init();
    }

    protected function initBasicColumns($columns)
    {
        //non-foreign columns
        foreach ($columns as $column)
        {
            if (count($column->getForeignKeys()) == 0)
            {
                $this->addColumn(ColumnFactory::create($column, $this), $column->name);
            }
            else
            {
                $this->columnAliases[$column->name] = WDB\Utils\Arrays::first($column->getForeignKeys())->name;
            }
        }
    }

    protected function initForeignColumns($fKeys)
    {
        //foreign columns
        foreach ($fKeys as $key)
        {
            if (isset($this->columns[$key->name])) throw new Exception\KeyException ("FK constraint name {$key->name} is duplicit to a column name in the same table");
            $this->addColumn(ColumnFactory::createForeign($key, $this, $this->tableAnalyzer->columns), $key->name);
        }
    }

    /**
     * for initialization of class descendants through overriding this method
     */
    protected function init()
    {

    }

    // <editor-fold defaultstate="collapsed" desc="source table and datasource query creators">

    /**
     * Get Query object bound to database of this table with this table prepared as source table.
     *
     * @return WDB\Query\Select
     */
    protected function getSourceTableQuery()
    {
        $q = new WDB\Query\Select(array(), $this->tableAnalyzer->identifier);
        $q->setDatabase($this->database);
        return $q;
    }

    public function getDatasource(array $cfg = array())
    {
        $ds = $this->getSourceTableQuery();
        foreach ($this->readFields as $field)
        {
            $ds->addField($field);
        }
        $this->configureDatasource($ds, $cfg);
        return $ds;
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Countable implementation">
    public function count()
    {
        if ($this->useCache && $this->cache->count !== NULL)
        {
            return $this->cache->count;
        }
        else
        {
            $count = $this->getSourceTableQuery()->addField(new WDB\Query\Element\SelectField(WDB\Query\Element\DBFunction::countRows(), 'count'))->run()->singleValue();
            if ($this->useCache)
            {
                $this->cache->count = $count;
            }
            return $count;
        }

    }
    // </editor-fold>

    /**
     *
     * @return Query\iSelectResult
     */
    private function getCachedDBData()
    {
        if ($this->useCache && $this->cache->data !== NULL)
        {
            return $this->cache->data;
        }
        else
        {
            $ds = $this->getDatasource();
            $data = $ds->run($this->database);
            if ($this->useCache)
            {
                $this->cache->data = $data;
            }
            return $data;
        }
    }

    /**
     * Get table locator structure
     *
     * @return WDB\Structure\TableLocator
     */
    public function getTableLocator()
    {
        return new WDB\Structure\TableLocator(array(
            'table'=>$this->tableAnalyzer->getName(),
            'schema'=>$this->tableAnalyzer->schema->getName(),
            'connection'=>$this->database->getName(),
        ));
    }

    public function isUIDetail()
    {
        return $this->tableAnalyzer->isUIDetail();
    }

    public function isUIEdit()
    {
        return $this->tableAnalyzer->isUIEdit();
    }

    public function isUIDelete()
    {
        return $this->tableAnalyzer->isUIDelete();
    }

    // <editor-fold defaultstate="collapsed" desc="ArrayAccess implementation">

    public function offsetExists($offset)
    {
        return $this->getCachedDBData()->offsetExists($offset);
    }

    public function offsetGet($offset)
    {
        return $this->fetchRecord($this->getCachedDBData()->offsetGet($offset));
    }

    public function offsetSet($offset, $value)
    {
        return $this->getCachedDBData()->offsetSet($offset, $value);
    }

    public function offsetUnset($offset)
    {
        return $this->getCachedDBData()->offsetUnset($offset);
    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Iterator implementation">
    public function current()
    {
        return $this->fetchRecord($this->getCachedDBData()->current());
    }

    public function key()
    {
        return $this->getCachedDBData()->key();
    }

    public function next()
    {
        return $this->fetchRecord($this->getCachedDBData()->next());
    }

    public function rewind()
    {
        return $this->getCachedDBData()->rewind();
    }

    public function valid()
    {
        return $this->getCachedDBData()->valid();
    }
    // </editor-fold>
}